package com.example.service.impl;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.dto.*;
import com.example.entity.vo.request.AddCommentVo;
import com.example.entity.vo.request.TopicCreateVo;
import com.example.entity.vo.request.TopicUpdateVo;
import com.example.entity.vo.response.CommentVo;
import com.example.entity.vo.response.TopicDetailVo;
import com.example.entity.vo.response.TopicPreviewVo;
import com.example.entity.vo.response.TopicTopVo;
import com.example.mapper.*;
import com.example.service.NotificationService;
import com.example.service.TopicService;
import com.example.utils.CacheUtils;
import com.example.utils.Const;
import com.example.utils.FlowUtils;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static com.example.utils.Const.*;

@Service
public class TopicServiceImpl
        extends ServiceImpl<TopicMapper, TopicDto>
        implements TopicService  {

    @Resource
    private TopicTypeMapper topicTypeMapper;

    @Resource
    private TopicMapper topicMapper;

    @Resource
    private FlowUtils flowUtils;

    @Resource
    private CacheUtils cacheUtils;

    @Resource
    private AccountMapper accountMapper;

    @Resource
    private AccountPrivacyMapper accountPrivacyMapper;

    @Resource
    private AccountDetailsMapper accountDetailsMapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private TopicCommentMapper topicCommentMapper;


    @Resource
    private NotificationService notificationService;

    //预处理
    private Set<Integer> types = null;
    @PostConstruct
    private void initTypes() {
        types = this.getTopicType()
                .stream()
                .map(TopicTypeDto::getId)
                .collect(Collectors.toSet());
    }

    @Override
    public List<TopicTypeDto> getTopicType() {
        //不需要条件
        return topicTypeMapper.selectList(null);
    }

    @Override
    public String createTopic(int uid, TopicCreateVo vo) {
        if(!textLimitCheck(vo.getContent(), 20000))
            return "文章内容太多，发文失败！";
        if(!types.contains(vo.getType()))
            return "文章类型非法！";
        String key = FORUM_TOPIC_CREATE_COUNTER + uid;
        if(!flowUtils.limitPeriodCounterCheck(key, 3, 3600))
            return "发文频繁，请稍后再试！";
        //封装文章
        TopicDto topic = new TopicDto();
        BeanUtils.copyProperties(vo, topic);//拷贝已有的数据
        topic.setContent(vo.getContent().toJSONString());
        topic.setUid(uid);
        topic.setTime(new Date());
        if(this.save(topic)) {
            //清除缓存数据
            cacheUtils.deleteCachePattern(FORUM_TOPIC_PREVIEW_CACHE + "*");
            return null;
        } else {
            return "内部错误，请联系管理员！";
        }

    }

    @Override
    public List<TopicPreviewVo> listTopicByPage(int pageNumber, int type) {
        String key = Const.FORUM_TOPIC_PREVIEW_CACHE + pageNumber + ":" + type;
        //读缓存
        List<TopicPreviewVo> list = cacheUtils.takeListFromCache(key, TopicPreviewVo.class);
        if(list != null)
            return list;
        //分页查询
        Page<TopicDto> page = Page.of(pageNumber, 10);
        //指定类型
        if(type == 0)
            baseMapper.selectPage(page, Wrappers.<TopicDto>query().orderByDesc("time"));
        else
            baseMapper.selectPage(page, Wrappers.<TopicDto>query().eq("type", type).orderByDesc("time"));
        List<TopicDto> topics = page.getRecords();
        if(topics.isEmpty()) return null;
        //封装数据
        list = topics.stream().map(this::resolveToPreview).toList();
        cacheUtils.saveListToCache(key, list, 60);
        return list;
    }

    @Override
    public List<TopicTopVo> listTopTopics() {
        List<TopicDto> topics = baseMapper.selectList(Wrappers.<TopicDto>query()
                .select("id", "title", "time")
                .eq("top", 1));
        return topics.stream().map(topic -> {
            TopicTopVo vo = new TopicTopVo();
            BeanUtils.copyProperties(topic, vo);
            return vo;
        }).toList();
    }

    @Override
    public TopicDetailVo getTopic(int tid, int uid) {
        TopicDetailVo vo = new TopicDetailVo();
        TopicDto topic = baseMapper.selectById(tid);
        BeanUtils.copyProperties(topic,vo);
        //封装用户数据
        //new静态内部类
        TopicDetailVo.User vo_user = new TopicDetailVo.User();
        TopicDetailVo.Interact vo_interact = new TopicDetailVo.Interact(
                this.hasInteract(tid,uid,"like"),
                this.hasInteract(tid,uid,"collect")
        );
        vo.setInteract(vo_interact);
        //根据用户隐私设置查询用户信息
        vo.setUser(fillUserDetailsByPrivacy(vo_user,topic.getUid()));
        //查找帖子评论数量
        vo.setComments(topicCommentMapper.selectCount(Wrappers.<TopicCommentDto>query().eq("tid",tid)));
        return vo;
    }

    /**
     * 论坛交互数据更新会非常的频繁
     * 直接往mysql存压力太大，所以借助redis做一层缓存，并定时一次性入库所有的数据
     * 当数据更新到来时，创建一个定时任务，此任务在一定时间后执行，
     * 如果期间有数据到来，仅仅更新redis即可
     */
    @Override
    public void interact(InteractDto interactDto, boolean state) {
        String type = interactDto.getType();
        //上锁，防止大量数据同时到达
        synchronized (type.intern()){
            //使用hash表，防止用户多次点赞
            //没法直接存boolean类型
//            stringRedisTemplate.opsForHash().put(type,interactDto.toKey(),state);
            stringRedisTemplate.opsForHash().put(type,interactDto.toKey(),Boolean.toString(state));
            //设置定时任务
            this.saveInteractSchedule(type);
        }
    }

    @Override
    public List<TopicPreviewVo> listTopicCollects(int uid) {
        return baseMapper.collectTopics(uid)
                .stream()
                .map(topic -> {
                    TopicPreviewVo vo = new TopicPreviewVo();
                    BeanUtils.copyProperties(topic, vo);
                    return vo;
                })
                .toList();
    }

    @Override
    public String updateTopic(int uid, TopicUpdateVo vo) {
        if(!textLimitCheck(vo.getContent(), 20000))
            return "文章内容太多，发文失败！";
        if(!types.contains(vo.getType()))
            return "文章类型非法！";
        int update = baseMapper.update(null, Wrappers.<TopicDto>update()
                .eq("uid", uid)
                .eq("id", vo.getId())
                .set("title", vo.getTitle())
                .set("content", vo.getContent().toString())
                .set("type", vo.getType())
        );
        //更新后清缓存
        if (update > 0){
            //清除缓存数据
            cacheUtils.deleteCachePattern(FORUM_TOPIC_PREVIEW_CACHE + "*");
            return null;
        }
        return "内部错误，请联系管理员";
    }

    @Override
    public String createComment(int uid, AddCommentVo vo) {
        //一分钟允许发15条评论
        String key = FORUM_TOPIC_COMMENT_COUNTER + uid;
        if(!flowUtils.limitPeriodCounterCheck(key, 15, 60))
            return "评论频繁，请稍后再试！";
        if(!textLimitCheck(JSONObject.parseObject(vo.getContent()), 2000))
            return "评论内容太多，发文失败！";
        TopicCommentDto dto = new TopicCommentDto();
        BeanUtils.copyProperties(vo, dto);
        dto.setTime(new Date());
        dto.setUid(uid);
        topicCommentMapper.insert(dto);
        //消息提醒
        TopicDto topic = baseMapper.selectById(vo.getTid());
        AccountDto account = accountMapper.selectById(uid);
        if (vo.getQuote() > 0){
            //说明是评论别人的评论
            //查出是谁的评论
            TopicCommentDto comment = topicCommentMapper.selectById(vo.getQuote());
            if (!Objects.equals(account.getId(), comment.getUid())){
                notificationService.addNotification(
                        comment.getUid(),
                        "您有新的帖子评论回复",
                        account.getUsername()+"回复你发表的评论，快去看看吧",
                        "success", "/index/topic-detail/"+comment.getTid()
                );
            }else if (!Objects.equals(account.getId(), topic.getUid())) {
                notificationService.addNotification(
                        topic.getUid(),
                        "您有新的帖子回复",
                        account.getUsername()+" 回复了你发表主题: "+topic.getTitle()+"，快去看看吧！",
                        "success", "/index/topic-detail/"+topic.getId()
                );
            }
        }

        return null;
    }

    @Override
    public List<CommentVo> comments(int tid, int pageNumber) {
        //1.先查出基本信息
        Page<TopicCommentDto> page = Page.of(pageNumber,10);
        //查出所有tid对应帖子的评论
        topicCommentMapper.selectPage(page,Wrappers.<TopicCommentDto>query()
                .eq("tid",tid));
        //TODO:stream和Lambda表达式的运用，优雅，太优雅
        return page.getRecords().stream().map(dto->{
            CommentVo vo = new CommentVo();
            //拷贝除了quote和用户信息之外的信息
            BeanUtils.copyProperties(dto,vo);
            //2.处理引用的信息
            if (dto.getQuote() > 0){
                //说明当前评论不是针对帖子，而是另一条评论
                //查出目标评论的内容（delta格式->JSON格式）
                TopicCommentDto commentDto = topicCommentMapper
                        .selectOne(Wrappers.<TopicCommentDto>query()
                                .eq("id", dto.getQuote())
                                .orderByAsc("time"));
                if (commentDto == null){
                    //说明引用的评论已经删除
                    vo.setQuote("此评论已被删除");
                }else {
                    //提取其中的内容
                    JSONObject object = JSONObject.parseObject(commentDto.getContent());
                    //简化内容
                    StringBuilder sb = new StringBuilder();
                    this.shortContent(object.getJSONArray("ops"),sb,ignore->{});//图片不做处理
                    vo.setQuote(sb.toString());
                }
            }
            //3.处理用户信息
            CommentVo.User vo_user = new CommentVo.User();
            this.fillUserDetailsByPrivacy(vo_user, dto.getUid());
            vo.setUser(vo_user);
            return vo;
        }).toList();
    }

    @Override
    public void deleteComment(int id, int uid) {
        topicCommentMapper.delete(Wrappers.<TopicCommentDto>query().eq("id", id).eq("uid", uid));
    }

    /**
     * 处理内容以生成预览文本，并对图片执行自定义操作。
     * @param ops JSONArray，包含不同类型的操作，每个操作可能是插入文本或图片。
     * @param previewText 用于构建预览文本的StringBuilder对象。
     * @param imageHandler 一个Consumer函数接口，用于处理找到的图片对象。
     */
    private void shortContent(JSONArray ops, StringBuilder previewText, Consumer<Object> imageHandler){
        // 遍历操作数组
        for (Object op : ops) {
            // 将操作对象转换为JSONObject，并尝试获取“insert”键对应的值
            Object insert = JSONObject.from(op).get("insert");
            // 检查insert是否为String类型（即文本）
            if(insert instanceof String text) {
                // 如果当前预览文本长度已经达到或超过300个字符，则跳过当前循环迭代
                if(previewText.length() >= 300) continue;
                // 将文本追加到previewText中
                previewText.append(text);
            } else if(insert instanceof Map<?, ?> map) {
                // 检查insert是否为Map类型（可能包含图片信息）
                // 注：delta数据中的图片也是一层JSON，JSON则是一种特殊的Map类型
                // 尝试从map中获取“image”键对应的值，如果存在，则执行imageHandler消费者操作
                Optional.ofNullable(map.get("image")).ifPresent(imageHandler);
            }
        }
    }


    //保存到数据库
    private void saveInteract(String type){
        synchronized (type.intern()){
            //两拨操作，操作和取消
            List<InteractDto> check = new LinkedList<>();
            List<InteractDto> uncheck = new LinkedList<>();
            //从缓存中取出
            stringRedisTemplate.opsForHash().entries(type)
                    .forEach((k,v)->{
                        if (Boolean.parseBoolean(v.toString())){
                            check.add(InteractDto.parseInteract(k.toString(),type));
                        }else {
                            uncheck.add(InteractDto.parseInteract(k.toString(),type));
                        }
                    });
            //批量入库
            if(!check.isEmpty()){
                baseMapper.addInteract(check,type);
            }
            if(!uncheck.isEmpty()){
                baseMapper.deleteInteract(uncheck,type);
            }
            //删除hash表
            stringRedisTemplate.delete(type);
        }
    }

    //TODO：定时任务（DOC）
// 创建一个状态映射，用于跟踪不同类型的任务是否已经被安排执行。
    private final Map<String, Boolean> state = new HashMap<>();

    // 创建一个ScheduledExecutorService实例，配置为拥有2个线程的线程池。
// 这允许同时安排和执行多个任务。
    ScheduledExecutorService service = Executors.newScheduledThreadPool(2);

    // 定义一个方法来安排保存交互数据的任务。
    private void saveInteractSchedule(String type) {
        // 检查给定类型的任务是否已经被安排执行。
        // 如果没有（即状态映射中没有该类型或其值为false），则进入条件块。
        if(!state.getOrDefault(type, false)) {
            // 将给定类型的状态设置为true，表示该类型的任务正在被安排。
            state.put(type, true);

            // 使用ScheduledExecutorService安排一个延迟执行的任务。
            // 具体来说，将在3秒后执行传递给schedule方法的Lambda表达式。
            service.schedule(() -> {
                // 调用saveInteract方法来处理实际的业务逻辑。
                // 注意：这里假设saveInteract是一个存在的方法，负责保存交互数据。
                this.saveInteract(type);

                // 任务执行完成后，将给定类型的状态设置回false，
                // 表示关于该类型的任务可以再次被安排执行。
                state.put(type, false);
            }, 3, TimeUnit.SECONDS); // 延迟时间设置为3秒。
        }
    }

    //读取帖子交互信息
    private boolean hasInteract(int tid,int uid,String type){
//      1.先从缓存里读取
        String key = tid +":" +uid;
        if(stringRedisTemplate.opsForHash().hasKey(type,key)){
//            return Boolean.parseBoolean(Objects.requireNonNull(stringRedisTemplate.opsForHash().get(type, key)).toString());
            return Boolean.parseBoolean(Objects.requireNonNull(stringRedisTemplate.opsForHash().entries(type).get(key).toString()));
        }
//      2.从数据库中读取
        return baseMapper.userInteractCount(tid,uid,type)>0;
    }


    //TODO：反射和泛型的运用
    //根据用户隐私设置，封装用户信息
    private <T> T fillUserDetailsByPrivacy(T target,int uid){
        //查出用户的信息和隐私设置，根据隐私设置选择展示哪些
        AccountDto account = accountMapper.selectById(uid);
        AccountDetailsDto details = accountDetailsMapper.selectById(uid);
        AccountPrivacyDto privacy = accountPrivacyMapper.selectById(uid);
        //拿到需要隐藏的字段
        String[] hiddenFields = privacy.hiddenFields();
        //拷贝
        BeanUtils.copyProperties(account, target, hiddenFields);
        BeanUtils.copyProperties(details, target, hiddenFields);
        return target;
    }

    //检查ops文本长度
    private boolean textLimitCheck(JSONObject object, int max) {
        if(object == null) return false;
        long length = 0;
//        统计所有ops对象的insert属性中文本的长度
        for (Object op : object.getJSONArray("ops")) {
            length += JSONObject.from(op).getString("insert").length();
            if(length > max) return false;
        }
        return true;
    }


    //将dto映射为vo
    private TopicPreviewVo resolveToPreview(TopicDto topic) {
        TopicPreviewVo vo = new TopicPreviewVo();
        //拷贝已有的参数
        BeanUtils.copyProperties(accountMapper.selectById(topic.getUid()), vo);
        BeanUtils.copyProperties(topic, vo);

        //文本、图片
        List<String> images = new ArrayList<>();
        StringBuilder previewText = new StringBuilder();

        //解析内容
        JSONArray ops = JSONObject.parseObject(topic.getContent()).getJSONArray("ops");
        for (Object op : ops) {
            Object insert = JSONObject.from(op).get("insert");
            //有可能是文本，有可能是图片
            if(insert instanceof String text){
                //文本
                if (previewText.length()>=300) continue;
                previewText.append(text);
            }else if(insert instanceof Map<?,?> map){
                //图片:用的JSON存，JSON本身是有一种Map
                Optional.ofNullable(map.get("image"))
                        .ifPresent(obj->images.add(obj.toString()));
            }
        }
        //保存300个字
        vo.setText(previewText.length() > 300 ? previewText.substring(0, 300) : previewText.toString());
        vo.setImages(images);

        //封装交互信息
        vo.setLike(
                baseMapper.interactCount(topic.getId(),"like")
        );
        vo.setCollect(
                baseMapper.interactCount(topic.getId(),"collect")
        );

        return vo;
    }
}
